在開發大型應用時,確保用戶只能訪問他們有權限的頁面是非常重要的。Vue Router 提供了靈活的路由控制能力,可以結合角色權限實現應用的安全性。本文將探討如何使用 Vue Router 設計基於角色的路由權限控制,並介紹 ABAC (Attribute-Based Access Control) 和 RBAC (Role-Based Access Control) 兩種常見的訪問控制模型。
RBAC (Role-Based Access Control):基於角色的訪問控制,是最常見的權限控制模型之一。用戶被賦予一個或多個角色,而每個角色對應特定的權限,進而決定用戶能訪問的資源。例如,"管理員" 角色擁有訪問所有頁面的權限,而 "普通用戶" 可能只能訪問部分頁面。
ABAC (Attribute-Based Access Control):基於屬性的訪問控制,除了角色之外,還根據用戶屬性(如年齡、地點、使用設備等)來決定是否允許訪問。這種模型更靈活,但實現相對複雜,適合對安全性要求較高的應用。
在這篇文章中,我們將設計一個簡單的 RBAC 實現,並結合一些 ABAC 的概念,來控制用戶訪問不同的路由。
首先,我們定義一組角色和對應的路由權限。這些角色將用於控制用戶的訪問範圍。
(檔案: src/schemas/auth/roles.ts
)
export enum Roles {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
// 定義每個角色的訪問權限
export const rolePermissions = {
[Roles.ADMIN]: ['dashboard', 'settings', 'profile'] as const,
[Roles.USER]: ['dashboard', 'profile'] as const,
[Roles.GUEST]: ['home'] as const,
} as const;
export type RolePermissionMap = typeof rolePermissions;
export type PermissionForRoles<R extends Roles> = RolePermissionMap[R][number];
export type AllPermissions = RolePermissionMap[Roles][number];
export const roleValidator = <R extends Roles>(role: R, route: AllPermissions): route is PermissionForRoles<R> => {
const roleRouteList = rolePermissions[role];
return roleRouteList.findIndex(item => item === route) !== -1;
};
這裡定義了三個角色:ADMIN
、USER
和 GUEST
,並為每個角色設置了可訪問的頁面。
接下來,我們設置 Vue Router 並實現一個簡單的路由守衛來控制訪問權限。
(檔案: src/rouer/index.ts
)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router';
import { Roles } from '../schemas/auth/roles'
export enum RouteStatus {
Home = 'home',
Dashboard = 'dashboard',
Settings = 'settings',
Profile = 'profile',
}
// 定義路由
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: RouteStatus.Home,
component: () => import('../pages/Home.vue'),
meta: {
requiredRoles: [
Roles.GUEST,
Roles.USER,
Roles.ADMIN
]
}
},
{
path: '/dashboard',
name: RouteStatus.Dashboard,
component: () => import('../pages/Dashboard.vue'),
meta: {
requiredRoles: [
Roles.USER,
Roles.ADMIN
]
}
},
{
path: '/settings',
name: RouteStatus.Settings,
component: () => import('../pages/Settings.vue'),
meta: {
requiredRoles: [
Roles.ADMIN
]
}
},
{
path: '/profile',
name: RouteStatus.Profile,
component: () => import('../pages/Profile.vue'),
meta: {
requiredRoles: [
Roles.USER,
Roles.ADMIN
]
}
},
];
const router = createRouter({
history: createWebHistory(),
routes,
});
// 模擬當前用戶角色
const currentUserRole = Roles.USER; // 這裡可以根據實際情況設置用戶角色
const isStringArray = (input: unknown): input is string[] => {
if (!Array.isArray(input)) return false;
return input.every(item => typeof item === 'string');
};
// 路由守衛:根據角色控制訪問
router.beforeEach(to => {
const requiredRoles = to.meta.requiredRoles;
if (!isStringArray(requiredRoles)) {
return { name: RouteStatus.Home }
}
if (requiredRoles && !requiredRoles.includes(currentUserRole)) {
// 如果當前角色無權訪問,則到首頁
return { name: RouteStatus.Home }
}
return true;
});
export default router;
這個路由守衛根據每個路由的 meta.requiredRoles
屬性來檢查當前用戶是否有權訪問該頁面。如果當前角色不在允許訪問的角色列表中,用戶將被重定向到首頁。
雖然 RBAC 是最基本的控制方式,我們可以進一步使用 ABAC 的思想來對訪問進行更細緻的控制,例如根據用戶屬性來決定訪問權限。這裡我們可以配合使用 zod
進行更細緻的操作。
(檔案: src/schemas/auth/abac.ts
)
import { Roles } from './roles';
// 假設這是用戶信息,包含一些屬性
const currentUser = {
role: Roles.USER,
email: 'test@gmail.com',
age: 25,
location: 'Taiwan',
isEmailVerify: true,
};
// 檢查用戶屬性是否允許訪問
import * as zod from 'zod';
// 用戶可以限制地點,年齡, 有沒有驗證 email, 某些 email, 某些角色 才可以通過,(null 則不限制)
export const checkAllowAccess = (
user: unknown,
constraintRoles: string[] | null, // 限制角色
constraintEmails: string[] | null, // 限制 email,
constraintAgeRange: [number, number] | null, // 限制年齡 (未滿 18 歲不可以看)
constraintLocations: string[] | null, // 限制地點 (只有台灣地區才可以看)
constraintEmailVerify?: boolean, // 是否郵件通過驗證才可以看
): boolean => {
const userInformationSchema = zod.object({
roles: zod.string().refine(val => {
if (constraintRoles === null) return true;
return constraintRoles.includes(val);
}),
email: zod.string().refine(val => {
if (constraintEmails === null) return true;
return constraintEmails.includes(val);
}),
age: zod.number().refine(val => {
if (constraintAgeRange === null) return true;
if (constraintAgeRange[1] < constraintAgeRange[0]) return false;
const conditionList: boolean[] = [];
if (constraintAgeRange[0] !== -1) {
conditionList.push(val > constraintAgeRange[0]);
}
if (constraintAgeRange[1] !== -1) {
conditionList.push(val < constraintAgeRange[1]);
}
return conditionList.every(item => item === true);
}),
location: zod.string().refine(val => {
if (constraintLocations === null) return true;
return constraintLocations.includes(val);
}),
isEmailVerify: zod.boolean().refine(val => {
if (!constraintEmailVerify) return true;
return val;
}),
});
const { success } = userInformationSchema.safeParse(user);
return success;
};
這裡設置了一個簡單的 ABAC 驗證邏輯,當用戶訪問 settings
頁面時,將根據年齡屬性進行額外的驗證。
我們可以在路由守衛中加入 ABAC 檢查,以實現更靈活的訪問控制。
(檔案: src/router/index.ts
)
import { checkAllowAccess } from '../schemas/auth/abac'
router.beforeEach(to => {
const requiredRoles = to.meta.requiredRoles;
if (!isStringArray(requiredRoles)) {
return { name: RouteStatus.Home }
}
if (requiredRoles && !requiredRoles.includes(currentUserRole)) {
// 如果當前角色無權訪問,則到首頁
return { name: RouteStatus.Home }
}
// 這裡更細緻的判斷 利用 ABAC 的概念
if (!checkAllowAccess(
currentUser,
null, // 不限制角色
null, // 不限制 email
[18, -1], // 18 歲以上
['Taiwan'], // 只有台灣能看
)) {
return { name: RouteStatus.Home }
}
return true;
});
這樣,我們在進行角色檢查後,再使用 ABAC 進行額外的屬性驗證,進一步提升安全性和靈活性。通常 ABAC 會伴隨著一整套複雜邏輯在 pinia 管理,我們這裡簡單示範一下 ABAC 的整體應用,有可能相關資料,限制條件可能會放在 meta
處理,就像 RBAC
的處理方式一樣。
通過結合 Vue Router、RBAC 和 ABAC,我們可以實現靈活且安全的路由訪問控制。RBAC 能夠快速實現基於角色的訪問控制,而 ABAC 則提供了更細緻的屬性檢查能力,使得我們可以根據應用需求設計更複雜的訪問邏輯。
希望通過這篇文章,能幫助你在 Vue 應用中設計出合理的角色權限控制機制,提升應用的安全性和用戶體驗。